﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Documents;
using System.Collections.ObjectModel;
using System.Windows;
using System.Windows.Media.TextFormatting;
using System.Windows.Media;
using System.Windows.Controls;
using System.Windows.Markup;
using System.Collections.Specialized;
using SDF.Primitives;
using SDF.TextFormatting;
using System.Collections;
using System.ComponentModel;
using SDF.Pagination;

namespace SDF
{
    [ContentProperty("Inlines")]
    public class Paragraph : SmartBlock, IInlineContainer 
    {
        #region Constructor

        public Paragraph()
        {
            Inlines = new SmartInlineCollection(this);
            AttachCollection(Inlines, true);

            Canvas = new Canvas();
            Canvas.Height = 0;
            Canvas.Width = 0;

            Figures = new List<Figure>();
            Lines = new List<LineElement>();
        }

        #endregion

        #region DependencyProperties

        public static readonly DependencyProperty KeepTogetherProperty =
            System.Windows.Documents.Paragraph.KeepTogetherProperty.AddOwner(typeof(Paragraph));

        #endregion

        #region Properties

        [DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
        public SmartInlineCollection Inlines { get; private set; }

        protected Canvas Canvas { get; private set; }

        protected List<Figure> Figures { get; private set; }

        protected List<LineElement> Lines { get; private set; }

        public bool KeepTogether
        {
            get { return (bool)GetValue(KeepTogetherProperty); }
            set { SetValue(KeepTogetherProperty, value); }
        }

        #endregion

        #region Visual Children Implementation

        protected override int VisualChildrenCount
        {
            get { return 1; }
        }

        protected override Visual GetVisualChild(int index)
        {
            if (index < 0 || index >= 1)
                throw new ArgumentOutOfRangeException("index");

            return Canvas;
        }

        #endregion

        #region Measure and Arrange Methods

        protected override Size MeasureOverride(Size availableSize)
        {
            Canvas.Children.Clear();

            if (Inlines.Count > 0)
            {
                Canvas.MaxWidth = Math.Max(availableSize.Width, 1.0/300);
                currentAvailableSize = new Size(Canvas.MaxWidth, availableSize.Height);
                MeasureFigures();

                Lines.Clear();
                if (!double.IsNaN(Canvas.MaxWidth) && !double.IsInfinity(Canvas.MaxWidth))
                {
                    PositionAllFigures();
                    PositionLines();
                }
                else
                {
                    Canvas.MaxWidth = MaximumWidth;
                    PositionAllFigures();
                    PositionLines();
                    RepositionRightFigures();
                }
                UpdateSize();
            }

            Canvas.Measure(availableSize);
            var result = new Size(Canvas.Width, Canvas.Height);
            return result;
        }
        private Size currentAvailableSize;

        private void UpdateSize()
        {
            Canvas.Height = 0;
            Canvas.Width = 0;
            foreach (var figure in Figures)
            {
                var bounds = figure.Bounds;
                if (bounds.Bottom > Canvas.Height)
                    Canvas.Height = bounds.Bottom;
                if (bounds.Right > Canvas.Width)
                    Canvas.Width = bounds.Right;
            }
            foreach (var line in Lines)
            {
                var bounds = line.Bounds;
                if (bounds.Bottom > Canvas.Height)
                    Canvas.Height = bounds.Bottom;
                if (bounds.Right > Canvas.Width)
                    Canvas.Width = bounds.Right;
            }
        }

        public Size GetCurrentAvailableSize()
        {
            return currentAvailableSize;
        }

        protected override Size ArrangeOverride(Size availableSize)
        {
            // If we are given more or less space than we originally measured for, we have to re-measure as 
            // text positioning may have changed
            if (availableSize != Canvas.DesiredSize)
                MeasureOverride(availableSize);

            Canvas.Width = availableSize.Width;
            Canvas.Arrange(new Rect(availableSize));

            return new Size(availableSize.Width, Canvas.Height);
        }

        #endregion

        #region Padding Methods

        protected override Thickness CalculateActualPadding()
        {
            double left = Padding.Left;
            double top = Padding.Top;
            double right = Padding.Right;
            double bottom = Padding.Bottom;

            if (double.IsNaN(left))
                left = LineHeight;
            if (double.IsNaN(top))
                top = LineHeight;
            if (double.IsNaN(right))
                right = LineHeight;
            if (double.IsNaN(bottom))
                bottom = LineHeight;

            return new Thickness(left, top, right, bottom);
        }

        #endregion

        #region DataChanged

        protected override void OnDataChanged(DataChangedEventArgs e)
        {
            base.OnDataChanged(e);
            this.InvalidateRender();
        }

        #endregion

        #region Figure Methods

        private void MeasureFigures()
        {
            Figures = new List<Figure>();
            foreach (var inline in Inlines)
                ExtractFigures(inline, Figures);

            foreach (var figure in Figures)
                MeasureFigure(figure);
        }

        private void ExtractFigures(SmartInline inline, List<Figure> figures)
        {
            var figure = inline as Figure;
            if (figure != null)
            {
                figures.Add(figure);
                return;
            }

            var container = inline as IInlineContainer;
            if (container != null)
            {
                foreach(var child in container.Inlines)
                    ExtractFigures(child, figures);
            }
        }


        private void MeasureFigure(Figure figure)
        {
            var size = new Size(currentAvailableSize.Width, double.PositiveInfinity);
            Canvas.Children.Add(figure.GetVisual(size));
        }

        private void PositionAllFigures()
        {
            double leftTop = 0;
            double rightTop = 0;
            
            var positionedFigures = new List<Figure>();
            foreach (var figure in Figures)
            {
                if (figure.Alignment == FigureAlignment.Left)
                {
                    figure.Left = 0;
                    figure.Top = leftTop;

                    foreach (var previousFigure in positionedFigures)
                    {
                        if (CheckIfElementsIntersect(previousFigure.Bounds, figure.Bounds))
                            figure.Top = previousFigure.Bounds.Bottom;
                    }
                    leftTop = figure.Bounds.Bottom;
                }
                else
                {
                    figure.Left = (Canvas.MaxWidth - figure.DesiredSize.Width);
                    figure.Top = rightTop;

                    foreach (var previousFigure in positionedFigures)
                    {
                        if (CheckIfElementsIntersect(previousFigure.Bounds, figure.Bounds))
                            figure.Top = previousFigure.Bounds.Bottom;
                    }
                    rightTop = figure.Bounds.Bottom;
                }
                positionedFigures.Add(figure);
            }
        }

        private void RepositionRightFigures()
        {
            foreach (var figure in Figures.Where(f => f.Alignment == FigureAlignment.Right))
            {
                Rect bounds = figure.Bounds;
                bounds.X = 0;

                foreach (var leftFigure in Figures.Where(f => f.Alignment == FigureAlignment.Left))
                {
                    if (CheckIfElementsIntersect(leftFigure.Bounds, bounds))
                        bounds.X = leftFigure.Bounds.Right;
                }
                foreach (var line in Lines)
                {
                    if (CheckIfElementsIntersect(line.Bounds, bounds))
                        bounds.X = line.Bounds.Right;
                }
            }

            // Calculate final width
            double finalWidth = 0;
            foreach (var figure in Figures)
            {
                if (figure.Bounds.Right > finalWidth)
                    finalWidth = figure.Bounds.Right;
            }
            foreach(var line in Lines)
            {
                if (line.Bounds.Right > finalWidth)
                    finalWidth = line.Bounds.Right;
            }

            foreach (var figure in Figures.Where(f => f.Alignment == FigureAlignment.Right))
                figure.Left = (finalWidth - figure.DesiredSize.Width);
        }


        private bool CheckIfElementsIntersect(Rect rect1, Rect rect2)
        {
            if (rect1.IsEmpty || rect2.IsEmpty)
            {
                return false;
            }
            return ((((rect2.Left < rect1.Right) && (rect2.Right > rect1.Left)) && (rect2.Top < rect1.Bottom)) && (rect2.Bottom > rect1.Top));
        }

        #endregion

        #region Lines Methods

        private void PositionLines()
        {
            // Create a TextSource and a TextFormatter objects
            var textSource = new SmartTextSource(this.Inlines);
            var formatter = TextFormatter.Create(TextFormattingMode.Ideal);
            int index = 0;
            
            // Start at the top
            Rect lineBounds = GetLineBoundsStartingAt(0, 0);
            var paragraphProperties = new SmartTextParagraphProperties(this);

            // If we haven't specified a width in the measure, we can't align to center or right as the width will only be known during the arrange.
            if (Canvas.MaxWidth == MaximumWidth)
                paragraphProperties.SetTextAlignment(TextAlignment.Left);


            // Format each line of text from the text source and draw it.
            while (index < textSource.Length)
            {
                double remainingWidth = GetRemainingWidth(lineBounds.Width);
                if ((remainingWidth <= 0) && (Canvas.MaxWidth > 0))
                {
                    CurrentLine = new LineElement();
                    lineBounds = GetLineBoundsStartingAt(lineBounds.Bottom, 0);
                    continue;
                }

                // Create a textline from the text store using the TextFormatter object.
                using (var line = formatter.FormatLine(textSource,
                                                        index,
                                                        remainingWidth,
                                                        paragraphProperties,
                                                        null))
                {
                    if (line.HasOverflowed || line.IsTruncated)
                    {
                        // Can't fit the text
                        bool addLineBreak = false;
                        if (remainingWidth != lineBounds.Width)
                        {
                            // Means we already wrote something to this line, so we can safely add a line break and try again
                            addLineBreak = true;
                        }
                        if ((lineBounds.Width != Canvas.MaxWidth) && (Canvas.MaxWidth != 0))
                        {
                            // We haven't written anything. If the line's width is not the same as the paragraph's,
                            // it means we have figures and we can safely add line breaks until we reach the end of the figures
                            addLineBreak = true;
                        }

                        if (addLineBreak)
                        {
                            if (CurrentLine.Children.Count == 0)
                            {
                                AddInline(new InlineElement(0, line.Height), 0);
                                Canvas.SetTop(CurrentLine, lineBounds.Top);
                                Canvas.SetLeft(CurrentLine, lineBounds.Left);
                            }

                            CurrentLine = new LineElement();
                            lineBounds = GetLineBoundsStartingAt(lineBounds.Bottom, 0);
                            continue;
                        }
                        else
                        {
                            // Do nothing. We'll write the text anyway, even though it is truncated.
                            // this is because we have no way to make the paragraph widder, so we have no other choice than to truncate.

                            // Note that if we chose to add a line break and try again in this case, we would reach an infinite loop!
                        }
                    }

                    // Now that we know the actual height of the line, let's update the bounds and,
                    // if we did it, recheck to see if the content still fits
                    double height = Math.Max(line.Height, line.TextHeight);
                    if (height > lineBounds.Height)
                    {
                        lineBounds.Height = height;

                        foreach (var figure in Figures)
                        {
                            if (CheckIfElementsIntersect(lineBounds, figure.Bounds))
                            {
                                CurrentLine = new LineElement();
                                lineBounds = GetLineBoundsStartingAt(figure.Bounds.Bottom, 0);
                                continue;
                            }
                        }
                    }

                    // If we got here, the line fits
                    double trailingSpace = line.WidthIncludingTrailingWhitespace - line.Width;
                    var element = new InlineElement(line.Width, height);
                    AddInline(element, trailingSpace);
                    Canvas.SetTop(CurrentLine, lineBounds.Top);
                    Canvas.SetLeft(CurrentLine, lineBounds.Left);

                    using (var context = element.RenderOpen())
                    {
                        double y = line.TextHeight - line.Height;
                        // Draw the formatted text into the visual.
                        line.Draw(context, new Point(0, y), InvertAxes.None);
                    }

                    if ((line.NewlineLength > 0) || (line.HasOverflowed || line.IsTruncated))
                    {
                        // Line ends with a line-break, or the text was too long and is being clipped/truncated
                        CurrentLine = new LineElement();
                        lineBounds = GetLineBoundsStartingAt(lineBounds.Bottom, 0);
                    }

                    index = index + line.Length;
                }
            }
        }
        private LineElement CurrentLine;


        private Rect GetLineBoundsStartingAt(double top, double minWidth)
        {
            if (Lines.Count > 0)
            {
                var lastLine = Lines[Lines.Count - 1];
                top = lastLine.Bounds.Bottom;
            }

            double lineHeight = this.LineHeight;
            double left = 0;
            double width = Canvas.MaxWidth;

            var lineLocation = new Point(left, top);
            var lineBounds = new Rect(lineLocation, new Size(width, lineHeight));

            foreach (var figure in Figures)
            {
                if (figure.Alignment == FigureAlignment.Left)
                {
                    if (CheckIfElementsIntersect(lineBounds, figure.Bounds))
                    {
                        left = figure.Bounds.Right;
                        width = (lineBounds.Right - figure.Bounds.Right);
                        if (width <= minWidth)
                            return GetLineBoundsStartingAt(figure.Bounds.Bottom, minWidth);

                        // Update location
                        lineLocation = new Point(left, top);
                        lineBounds = new Rect(lineLocation, new Size(width, lineHeight));
                    }
                }
                else
                {
                    if (CheckIfElementsIntersect(lineBounds, figure.Bounds))
                    {
                        width = (figure.Bounds.Left - lineBounds.Left);
                        if (width <= minWidth)
                            return GetLineBoundsStartingAt(figure.Bounds.Bottom, minWidth);

                        // Update location
                        lineLocation = new Point(left, top);
                        lineBounds = new Rect(lineLocation, new Size(width, lineHeight));
                    }
                }
            }

            return lineBounds;
        }


        private double GetRemainingWidth(double width)
        {
            if (CurrentLine == null)
                return width;

            return CurrentLine.GetRemainingWidth(width);
        }


        private void AddInline(InlineElement element, double trailingSpace)
        {
            if (CurrentLine == null)
                CurrentLine = new LineElement();

            CurrentLine.AddInline(element, trailingSpace);
            if (!Lines.Contains(CurrentLine))
            {
                Lines.Add(CurrentLine);
                Canvas.Children.Add(CurrentLine);
            }
        }

        #endregion

        #region Constants

        public const double MaximumWidth = 3579139.4066666667;

        #endregion

        #region Harvest Method

        public override HarvestResult Harvest(HarvestingContext context, double remainingHeight, SmartBlock previousBlock)
        {
            bool isPartiallyHarvested = IsPartiallyHarvested(context);

            // If we are at the top level and we follow another element,
            // we may need to break the page or column
            if (PageContent.IsDirectChild(this) && (previousBlock != null) && !isPartiallyHarvested)
            {
                if (this.BreakPageBefore)
                    return HarvestResult.BreakPage;

                if (this.BreakColumnBefore)
                    return HarvestResult.BreakColumn;
            } 
            

            // If this is the first part, count the margin
            var harvestedMargin = this.ActualMargin;
            if (isPartiallyHarvested)
                harvestedMargin.Top = 0;
            else if ((previousBlock == null) && double.IsNaN(this.Margin.Top))
                harvestedMargin.Top = 0;
            remainingHeight -= harvestedMargin.Top;

            // Now, let's see if we can fit in the minimum height
            if (!isPartiallyHarvested && (this.CalculateMinHeight() > remainingHeight))
            {
                // We can't fit the paragraph, so break the column
                return HarvestResult.BreakColumn;
            }

            // Now, let's find out where do we start this harvesting pass
            double harvestStartingPoint = GetStartingHarvestPoint(context);
            double nextHarvestPoint = harvestStartingPoint;
            double harvestEndPoint = nextHarvestPoint;
            bool isFullyHarvested = false;

            // Find out how much we can fit
            while ((harvestEndPoint == harvestStartingPoint) ||  // Make sure we harvest at least once, even if it doesn't fit. Content will be clipped though
                   (remainingHeight - (nextHarvestPoint - harvestStartingPoint) > 0))
            {
                harvestEndPoint = nextHarvestPoint;
                nextHarvestPoint = GetNextBreakingPoint(harvestEndPoint);

                if (nextHarvestPoint == harvestEndPoint)
                {
                    isFullyHarvested = true;
                    break; // Got to the end of the parapraph
                }
            }


            // Use a frozen block since we won't be splitting the inlines (pre-render), but rather the rendered visuals
            // (figures and lines)
            var harvestedBlock = new FrozenBlock();
            harvestedBlock.Width = Canvas.Width;
            harvestedBlock.Height = (harvestEndPoint - harvestStartingPoint);
            harvestedBlock.Background = this.Background;
            if (!isFullyHarvested)
                harvestedMargin.Bottom = 0; // We are going to split, so remove the bottom margin


            // Harvest everything between starting point and breaking point
            foreach (var figure in Figures)
            {
                if (context.IsFullyHarvested(figure))
                    continue;

                var bounds = figure.Bounds;
                if ((bounds.Top >= harvestStartingPoint) && (bounds.Top < harvestEndPoint))
                {
                    var copy = figure.GetVisualCopy();
                    Canvas.SetTop(copy, figure.Top - harvestStartingPoint);
                    harvestedBlock.Children.Add(copy);
                    context.MarkAsFullyHarvested(figure);
                }
            }

            foreach (var line in Lines)
            {
                if (context.IsFullyHarvested(line))
                    continue;

                var bounds = line.Bounds;
                if ((bounds.Top >= harvestStartingPoint) && (bounds.Top < harvestEndPoint))
                {
                    var copy = line.GetVisualCopy();
                    Canvas.SetTop(copy, line.Top - harvestStartingPoint);
                    harvestedBlock.Children.Add(copy);
                    context.MarkAsFullyHarvested(line);
                }
            }

            if (isFullyHarvested)
                context.MarkAsFullyHarvested(this);

            harvestedBlock.Margin = harvestedMargin;
            remainingHeight -= (harvestedBlock.Height + harvestedMargin.Bottom);

            return new HarvestResult(harvestedBlock, remainingHeight);
        }

        private double GetStartingHarvestPoint(HarvestingContext context)
        {
            double result = 0;
            foreach (var figure in Figures)
            {
                if (context.IsFullyHarvested(figure))
                {
                    var bounds = figure.Bounds;
                    if (bounds.Bottom > result)
                        result = bounds.Bottom;
                }
            }

            foreach (var line in Lines)
            {
                if (context.IsFullyHarvested(line))
                {
                    var bounds = line.Bounds;
                    if (bounds.Bottom > result)
                        result = bounds.Bottom;
                }
            }

            return result;
        }

        public override SmartBlock GetEmptyCopy()
        {
            var result = new Paragraph();
            CopyProperties(result);
            result.KeepTogether = this.KeepTogether;
            return result;
        }

        public override bool IsPartiallyHarvested(HarvestingContext context)
        {
            if (context.IsFullyHarvested(this))
                return false;

            foreach (var figure in Figures)
            {
                if (context.IsFullyHarvested(figure))
                    return true;
            }

            foreach (var line in Lines)
            {
                if (context.IsFullyHarvested(line))
                    return true;
            }
            
            return false;
        }

        #endregion

        #region CalculateMinHeight Method

        public override double CalculateMinHeight()
        {
            if (Canvas.Children.Count > 0)
            {
                if (this.KeepTogether)
                {
                    return Canvas.Height;
                }
                else
                {
                    return GetNextBreakingPoint(0);
                }
            }

            return 0;
        }

        #endregion

        #region GetNextBreakingPoint Method

        protected double GetNextBreakingPoint(double currentBreakingPoint)
        {
            double result = currentBreakingPoint;
            bool keepChecking = true;
            while (keepChecking)
            {
                keepChecking = false;
                foreach (var figure in Figures)
                {
                    var bounds = figure.Bounds;
                    if (((bounds.Top <= currentBreakingPoint) || (bounds.Top < result)) && (bounds.Top > result))
                    {
                        result = bounds.Bottom;
                        keepChecking = true;
                        break;
                    }
                }

                if (keepChecking)
                    continue;

                foreach (var line in Lines)
                {
                    var bounds = line.Bounds;
                    if (((bounds.Top <= currentBreakingPoint) || (bounds.Top < result)) && (bounds.Bottom > result))
                    {
                        result = bounds.Bottom;
                        keepChecking = true;
                        break;
                    }
                }
            }

            return result;
        }

        #endregion
    }
}
